Micron Document
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| SparkN0de-git | SparkN0de |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


Commit 2fe900061e4d5da639a129894494e020b4b686d9


Parents : febdc3b
Author : Ivan <e46112d44649266d71fe2193e00a4710>
Signature : T66BB85Valid, signed by author
Date : 2026-07-25T14:23:09-05:00

feat: update RNS to version 1.4.1, add gravity and announces-to-internal options in interfaces, and enhance interface management in the UI

Changes

35 files changed, 561 insertions(+), 34 deletions(-)

M CHANGELOG.md +2 -1
M uv.lock +4 -4

Diff

diff --git a/.github/actions/setup-dev-environment/action.yml b/.github/actions/setup-dev-environment/action.yml
index e3ecfcfd..e6ef044d 100644
--- a/.github/actions/setup-dev-environment/action.yml
+++ b/.github/actions/setup-dev-environment/action.yml
@@ -50,9 +50,9 @@ runs:
if command -v python >/dev/null 2>&1; then
py=python
fi
- if "$py" -m pip install --user 'rns>=1.4.0' 2>/dev/null; then
+ if "$py" -m pip install --user 'rns>=1.4.1' 2>/dev/null; then
:
- elif "$py" -m pip install --user --break-system-packages 'rns>=1.4.0' 2>/dev/null; then
+ elif "$py" -m pip install --user --break-system-packages 'rns>=1.4.1' 2>/dev/null; then
:
else
echo "WARNING: could not install rnid via pip (tree verify may be skipped)" >&2

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4ca6add2..1187dbd7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -25,7 +25,8 @@ All notable changes to this project will be documented in this file.
- Network visualiser WebGL + WASM renderer (vis-network fallback) and Settings renderer preference
- Interfaces: internal mode, recursive path requests, announces-from-internal, discovery location command, and Backbone fast-flapping options (RNS 1.3.7 to 1.3.9)
- Reticulum interface module management from the UI
-- Reticulum 1.4.0 and LXMF 1.1.0, with propagation node options and cancel for incoming large message downloads from the header
+- Reticulum 1.4.1 and LXMF 1.1.0, with propagation node options and cancel for incoming large message downloads from the header
+- Interfaces: path gravity, announces-to-internal, and discovery autoconnect gravity/mode options (RNS 1.4.1)
- Relay Chat room keys so hosts can require a key to join a room
- Desktop privacy: Windows screen security to omit MeshChatX from screenshots, recording, and Recall
- Android privacy options to block screenshots and clear the clipboard when backgrounded

diff --git a/android/app/build.gradle b/android/app/build.gradle
index 90509ef4..47737289 100644
--- a/android/app/build.gradle
+++ b/android/app/build.gradle
@@ -13,7 +13,7 @@ def pythonTempDir = new File(buildDir, "python-tmp")
def vendorWheelDir = new File(rootProject.projectDir, "vendor")
def lxstPatchedWheel = new File(rootProject.projectDir, "vendor/lxst-0.5.0-py3-none-any.whl")
def bleakPatchedWheel = new File(rootProject.projectDir, "vendor/bleak-3.0.2-py3-none-any.whl")
-def rnsPatchedWheel = new File(rootProject.projectDir, "vendor/rns-1.4.0-py3-none-any.whl")
+def rnsPatchedWheel = new File(rootProject.projectDir, "vendor/rns-1.4.1-py3-none-any.whl")
def allAndroidAbis = ["arm64-v8a", "x86_64", "armeabi-v7a"]
def selectedAndroidAbis = (
project.findProperty("meshchatxAbis")

diff --git a/android/app/src/main/java/com/meshchatx/rnode/UsbSerialHub.java b/android/app/src/main/java/com/meshchatx/rnode/UsbSerialHub.java
index 89dbbd07..aa84a780 100644
--- a/android/app/src/main/java/com/meshchatx/rnode/UsbSerialHub.java
+++ b/android/app/src/main/java/com/meshchatx/rnode/UsbSerialHub.java
@@ -290,16 +290,13 @@ public final class UsbSerialHub {
};
IntentFilter filter = new IntentFilter(ACTION_USB_PERMISSION);
try {
- if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
- ContextCompat.registerReceiver(
- appContext,
- permissionReceiver,
- filter,
- ContextCompat.RECEIVER_NOT_EXPORTED
- );
- } else {
- appContext.registerReceiver(permissionReceiver, filter);
- }
+ // App-private USB permission result. Not exported to other apps.
+ ContextCompat.registerReceiver(
+ appContext,
+ permissionReceiver,
+ filter,
+ ContextCompat.RECEIVER_NOT_EXPORTED
+ );
receiverRegistered = true;
} catch (Exception e) {
Log.w(TAG, "registerReceiver failed: " + e.getMessage());

diff --git a/docs/agents/skills/lxmf-messaging/SKILL.md b/docs/agents/skills/lxmf-messaging/SKILL.md
index 0cbf7133..3939d1d5 100644
--- a/docs/agents/skills/lxmf-messaging/SKILL.md
+++ b/docs/agents/skills/lxmf-messaging/SKILL.md
@@ -39,7 +39,7 @@ MeshChatX surfaces them as:
Outbound cancel remains `POST /api/v1/lxmf-messages/{hash}/cancel` via `cancel_outbound`.
-Keep minimum versions: `rns>=1.4.0`, `lxmf>=1.1.0`.
+Keep minimum versions: `rns>=1.4.1`, `lxmf>=1.1.0`.
## Gates

diff --git a/meshchatx.rsm b/meshchatx.rsm
index aef6f6e6..91bc1d18 100644
Binary files a/meshchatx.rsm and b/meshchatx.rsm differ

diff --git a/meshchatx/src/backend/data/THIRD_PARTY_NOTICES.txt b/meshchatx/src/backend/data/THIRD_PARTY_NOTICES.txt
index 0f054d80..c91070df 100644
--- a/meshchatx/src/backend/data/THIRD_PARTY_NOTICES.txt
+++ b/meshchatx/src/backend/data/THIRD_PARTY_NOTICES.txt
@@ -89,7 +89,7 @@ pyserial 3.5
reticulum-meshchatx 4.8.0
License: 0BSD AND MIT
Author: Quad4
-rns 1.4.0
+rns 1.4.1
License: Reticulum License
Author: Mark Qvist
rns-filesync 1.0.0

diff --git a/meshchatx/src/backend/data/licenses_backend.json b/meshchatx/src/backend/data/licenses_backend.json
index 93294833..cd67a215 100644
--- a/meshchatx/src/backend/data/licenses_backend.json
+++ b/meshchatx/src/backend/data/licenses_backend.json
@@ -50,7 +50,7 @@
{
"name": "cbor2",
"version": "6.1.1",
- "author": "Alex Grönholm <alex.gronholm@nextday.fi>",
+ "author": "Alex Gr\u00f6nholm <alex.gronholm@nextday.fi>",
"license": "MIT"
},
{
@@ -163,7 +163,7 @@
},
{
"name": "rns",
- "version": "1.4.0",
+ "version": "1.4.1",
"author": "Mark Qvist",
"license": "Reticulum License"
},

diff --git a/meshchatx/src/backend/http/routes/interfaces.py b/meshchatx/src/backend/http/routes/interfaces.py
index 2c654fe2..8a85921c 100644
--- a/meshchatx/src/backend/http/routes/interfaces.py
+++ b/meshchatx/src/backend/http/routes/interfaces.py
@@ -1721,6 +1721,33 @@ def register_interfaces_routes(routes, app):
},
status=422,
)
+ announces_to_error = InterfaceEditor.apply_yes_no_option(
+ interface_details,
+ data,
+ "announces_to_internal",
+ )
+ if announces_to_error is not None:
+ return web.json_response(
+ {
+ "message": announces_to_error,
+ },
+ status=422,
+ )
+ gravity_error = InterfaceEditor.apply_positive_number(
+ interface_details,
+ data,
+ "gravity",
+ as_int=True,
+ minimum=-10_000,
+ maximum=10_000,
+ )
+ if gravity_error is not None:
+ return web.json_response(
+ {
+ "message": gravity_error,
+ },
+ status=422,
+ )
InterfaceEditor.update_value(interface_details, data, "network_name")
InterfaceEditor.update_value(interface_details, data, "passphrase")
InterfaceEditor.update_value(interface_details, data, "ifac_size")

diff --git a/meshchatx/src/backend/http/routes/reticulum_instance.py b/meshchatx/src/backend/http/routes/reticulum_instance.py
index 881113b6..8e0e47b0 100644
--- a/meshchatx/src/backend/http/routes/reticulum_instance.py
+++ b/meshchatx/src/backend/http/routes/reticulum_instance.py
@@ -156,6 +156,16 @@ def register_reticulum_instance_routes(routes, app):
"autoconnect_discovered_interfaces",
ReticulumMeshChat.DEFAULT_AUTOCONNECT_DISCOVERED_INTERFACES,
),
+ "default_gravity": reticulum_config.get("default_gravity"),
+ "autoconnect_interface_mode": reticulum_config.get(
+ "autoconnect_interface_mode",
+ ),
+ "autoconnect_interface_gravity": reticulum_config.get(
+ "autoconnect_interface_gravity",
+ ),
+ "autoconnect_announces_to_internal": reticulum_config.get(
+ "autoconnect_announces_to_internal",
+ ),
"default_bootstrap_only": bool(
app.current_context.config.default_bootstrap_only.get()
if app.current_context and app.current_context.config
@@ -215,6 +225,67 @@ def register_reticulum_instance_routes(routes, app):
):
update_config_value(key)
+ if "autoconnect_interface_mode" in data:
+ mode_raw = data.get("autoconnect_interface_mode")
+ if mode_raw is None or mode_raw == "":
+ reticulum_config.pop("autoconnect_interface_mode", None)
+ else:
+ mode = InterfaceEditor.normalize_interface_mode(mode_raw)
+ if mode is None:
+ return web.json_response(
+ {
+ "message": (
+ "autoconnect_interface_mode must be one of: "
+ "full, gateway, access_point, pointtopoint, "
+ "roaming, boundary, internal"
+ ),
+ },
+ status=422,
+ )
+ reticulum_config["autoconnect_interface_mode"] = mode
+
+ if "autoconnect_announces_to_internal" in data:
+ yn = InterfaceEditor.request_yes_no(
+ data.get("autoconnect_announces_to_internal"),
+ )
+ if yn is None:
+ raw = data.get("autoconnect_announces_to_internal")
+ if raw is None or raw == "":
+ reticulum_config.pop("autoconnect_announces_to_internal", None)
+ else:
+ return web.json_response(
+ {
+ "message": (
+ "autoconnect_announces_to_internal must be "
+ "a boolean or yes/no value"
+ ),
+ },
+ status=422,
+ )
+ else:
+ reticulum_config["autoconnect_announces_to_internal"] = yn
+
+ for gravity_key in ("default_gravity", "autoconnect_interface_gravity"):
+ if gravity_key not in data:
+ continue
+ value = data.get(gravity_key)
+ if value is None or value == "":
+ reticulum_config.pop(gravity_key, None)
+ continue
+ try:
+ gravity = int(value)
+ except (TypeError, ValueError):
+ return web.json_response(
+ {"message": f"{gravity_key} must be an integer"},
+ status=422,
+ )
+ if gravity < -10_000 or gravity > 10_000:
+ return web.json_response(
+ {"message": f"{gravity_key} must be between -10000 and 10000"},
+ status=422,
+ )
+ reticulum_config[gravity_key] = gravity
+
# When discover_interfaces is off, also disable autoconnect so RNS
# does not connect to any discovered interfaces.
if "discover_interfaces" in data:
@@ -279,6 +350,16 @@ def register_reticulum_instance_routes(routes, app):
"autoconnect_discovered_interfaces",
ReticulumMeshChat.DEFAULT_AUTOCONNECT_DISCOVERED_INTERFACES,
),
+ "default_gravity": reticulum_config.get("default_gravity"),
+ "autoconnect_interface_mode": reticulum_config.get(
+ "autoconnect_interface_mode",
+ ),
+ "autoconnect_interface_gravity": reticulum_config.get(
+ "autoconnect_interface_gravity",
+ ),
+ "autoconnect_announces_to_internal": reticulum_config.get(
+ "autoconnect_announces_to_internal",
+ ),
"default_bootstrap_only": bool(
app.current_context.config.default_bootstrap_only.get()
if app.current_context and app.current_context.config

diff --git a/meshchatx/src/backend/interface_editor.py b/meshchatx/src/backend/interface_editor.py
index ab91d2e9..ca9bf43a 100644
--- a/meshchatx/src/backend/interface_editor.py
+++ b/meshchatx/src/backend/interface_editor.py
@@ -388,13 +388,27 @@ class InterfaceEditor:
"access_point, pointtopoint, roaming, boundary, internal"
)
iface_body["mode"] = mode
- for key in ("recursive_prs", "announces_from_internal", "block_fast_flapping"):
+ for key in (
+ "recursive_prs",
+ "announces_from_internal",
+ "announces_to_internal",
+ "block_fast_flapping",
+ ):
if key not in iface_body:
continue
yn = InterfaceEditor.request_yes_no(iface_body.get(key))
if yn is None:
return f"Imported interface {key} must be a boolean or yes/no value"
iface_body[key] = yn
+ if "gravity" in iface_body:
+ grav = iface_body.get("gravity")
+ if grav is None or grav == "":
+ iface_body.pop("gravity", None)
+ else:
+ try:
+ iface_body["gravity"] = int(grav)
+ except (TypeError, ValueError):
+ return "Imported interface gravity must be an integer"
if "location_cmd" in iface_body:
loc = iface_body.get("location_cmd")
if loc is None or loc == "":

diff --git a/meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue b/meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue
index 3d25b5f6..ea965c39 100644
--- a/meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue
+++ b/meshchatx/src/frontend/components/interfaces/AddInterfacePage.vue
@@ -1848,6 +1848,31 @@
</div>
<Toggle v-model="sharedInterfaceSettings.announces_from_internal" />
</div>
+ <div class="flex items-start justify-between gap-4">
+ <div class="min-w-0 max-w-md">
+ <FormLabel class="glass-label mb-0!">{{
+ $t("interfaces.announces_to_internal_label")
+ }}</FormLabel>
+ <p class="text-xs text-gray-400 mt-1">
+ {{ $t("interfaces.announces_to_internal_hint") }}
+ </p>
+ </div>
+ <Toggle v-model="sharedInterfaceSettings.announces_to_internal" />
+ </div>
+ <div>
+ <FormLabel class="glass-label">{{
+ $t("interfaces.gravity_label")
+ }}</FormLabel>
+ <input
+ v-model="sharedInterfaceSettings.gravity"
+ type="number"
+ :placeholder="$t('interfaces.gravity_placeholder')"
+ class="input-field"
+ />
+ <p class="text-xs text-gray-400 mt-1">
+ {{ $t("interfaces.gravity_hint") }}
+ </p>
+ </div>
</div>
<div class="space-y-4 pt-4 border-t border-gray-100 dark:border-zinc-800">
<FormLabel class="glass-label">Interface Access Code (IFAC)</FormLabel>
@@ -2221,6 +2246,8 @@ export default {
bitrate: null,
recursive_prs: false,
announces_from_internal: true,
+ announces_to_internal: false,
+ gravity: null,
},
discovery: {
@@ -2891,6 +2918,11 @@ export default {
iface.announces_from_internal === undefined || iface.announces_from_internal === null
? true
: this.parseBool(iface.announces_from_internal);
+ this.sharedInterfaceSettings.announces_to_internal = this.parseBool(iface.announces_to_internal);
+ this.sharedInterfaceSettings.gravity =
+ iface.gravity !== undefined && iface.gravity !== null && iface.gravity !== ""
+ ? Number(iface.gravity)
+ : null;
if (iface.frequency) {
this.RNodeGHzValue = Math.floor(iface.frequency / 1e9);
@@ -3079,6 +3111,16 @@ export default {
) {
this.sharedInterfaceSettings.announces_from_internal = this.parseBool(config.announces_from_internal);
}
+ if (
+ config.announces_to_internal !== undefined &&
+ config.announces_to_internal !== null &&
+ config.announces_to_internal !== ""
+ ) {
+ this.sharedInterfaceSettings.announces_to_internal = this.parseBool(config.announces_to_internal);
+ }
+ if (config.gravity !== undefined && config.gravity !== null && config.gravity !== "") {
+ this.sharedInterfaceSettings.gravity = Number(config.gravity);
+ }
if (
config.block_fast_flapping !== undefined &&
config.block_fast_flapping !== null &&
@@ -3196,6 +3238,16 @@ export default {
config.announces_from_internal !== ""
? this.parseBool(config.announces_from_internal)
: true,
+ announces_to_internal:
+ config.announces_to_internal !== undefined &&
+ config.announces_to_internal !== null &&
+ config.announces_to_internal !== ""
+ ? this.parseBool(config.announces_to_internal)
+ : false,
+ gravity:
+ config.gravity !== undefined && config.gravity !== null && config.gravity !== ""
+ ? this.numOrNull(config.gravity)
+ : null,
bitrate: this.numOrNull(config.bitrate),
network_name: config.network_name || null,
passphrase: config.passphrase || null,
@@ -3474,6 +3526,8 @@ export default {
mode: this.sharedInterfaceSettings.mode || null,
recursive_prs: this.sharedInterfaceSettings.recursive_prs === true,
announces_from_internal: this.sharedInterfaceSettings.announces_from_internal !== false,
+ announces_to_internal: this.sharedInterfaceSettings.announces_to_internal === true,
+ gravity: this.numOrNull(this.sharedInterfaceSettings.gravity),
bitrate: this.sharedInterfaceSettings.bitrate,
network_name: this.sharedInterfaceSettings.network_name,
passphrase: this.sharedInterfaceSettings.passphrase,
@@ -3631,6 +3685,8 @@ export default {
: this.sharedInterfaceSettings.mode || null,
recursive_prs: this.sharedInterfaceSettings.recursive_prs === true,
announces_from_internal: this.sharedInterfaceSettings.announces_from_internal !== false,
+ announces_to_internal: this.sharedInterfaceSettings.announces_to_internal === true,
+ gravity: this.numOrNull(this.sharedInterfaceSettings.gravity),
bitrate: this.sharedInterfaceSettings.bitrate,
network_name: this.sharedInterfaceSettings.network_name,
passphrase: this.sharedInterfaceSettings.passphrase,

diff --git a/meshchatx/src/frontend/components/interfaces/Interface.vue b/meshchatx/src/frontend/components/interfaces/Interface.vue
index 39927ffd..d37b8cbd 100644
--- a/meshchatx/src/frontend/components/interfaces/Interface.vue
+++ b/meshchatx/src/frontend/components/interfaces/Interface.vue
@@ -64,6 +64,9 @@
<span v-if="iface._stats?.bitrate" class="stat-chip"
>{{ $t("interface.bitrate") }} {{ formatBitsPerSecond(iface._stats?.bitrate ?? 0) }}</span
>
+ <span v-if="iface._stats?.gravity != null" class="stat-chip"
+ >{{ $t("interface.gravity") }} {{ iface._stats.gravity }}</span
+ >
<span class="stat-chip" :class="{ 'stat-chip--zero-traffic': isIfaceStatBytesZero('txb') }"
>{{ $t("interface.tx") }} {{ formatBytes(iface._stats?.txb ?? 0) }}</span
>

diff --git a/meshchatx/src/frontend/components/interfaces/InterfacesPage.vue b/meshchatx/src/frontend/components/interfaces/InterfacesPage.vue
index 6ac9b707..223db70c 100644
--- a/meshchatx/src/frontend/components/interfaces/InterfacesPage.vue
+++ b/meshchatx/src/frontend/components/interfaces/InterfacesPage.vue
@@ -652,6 +652,86 @@
0 disables auto-connect.
</div>
</div>
+ <div>
+ <div class="text-xs font-semibold text-gray-700 dark:text-gray-200">
+ {{ $t("interfaces.default_gravity_label") }}
+ </div>
+ <input
+ v-model.number="discoveryConfig.default_gravity"
+ type="number"
+ class="input-field"
+ />
+ <div class="text-xs text-gray-500 dark:text-gray-400">
+ {{ $t("interfaces.default_gravity_hint") }}
+ </div>
+ </div>
+ <div>
+ <div class="text-xs font-semibold text-gray-700 dark:text-gray-200">
+ {{ $t("interfaces.autoconnect_interface_mode_label") }}
+ </div>
+ <select
+ v-model="discoveryConfig.autoconnect_interface_mode"
+ class="input-field"
+ >
+ <option value="">
+ {{ $t("interfaces.mode_default_full") }}
+ </option>
+ <option value="full">
+ {{ $t("interfaces.mode_full") }}
+ </option>
+ <option value="gateway">
+ {{ $t("interfaces.mode_gateway") }}
+ </option>
+ <option value="access_point">
+ {{ $t("interfaces.mode_access_point") }}
+ </option>
+ <option value="roaming">
+ {{ $t("interfaces.mode_roaming") }}
+ </option>
+ <option value="boundary">
+ {{ $t("interfaces.mode_boundary") }}
+ </option>
+ <option value="internal">
+ {{ $t("interfaces.mode_internal") }}
+ </option>
+ </select>
+ <div class="text-xs text-gray-500 dark:text-gray-400">
+ {{ $t("interfaces.autoconnect_interface_mode_hint") }}
+ </div>
+ </div>
+ <div>
+ <div class="text-xs font-semibold text-gray-700 dark:text-gray-200">
+ {{ $t("interfaces.autoconnect_interface_gravity_label") }}
+ </div>
+ <input
+ v-model.number="discoveryConfig.autoconnect_interface_gravity"
+ type="number"
+ class="input-field"
+ />
+ <div class="text-xs text-gray-500 dark:text-gray-400">
+ {{ $t("interfaces.autoconnect_interface_gravity_hint") }}
+ </div>
+ </div>
+ <div class="sm:col-span-2">
+ <div
+ class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"
+ >
+ <div class="min-w-0 pr-0 sm:pr-4">
+ <div class="text-sm font-semibold text-gray-900 dark:text-white">
+ {{ $t("interfaces.autoconnect_announces_to_internal_label") }}
+ </div>
+ <div class="text-xs text-gray-500 dark:text-gray-400">
+ {{
+ $t("interfaces.autoconnect_announces_to_internal_hint")
+ }}
+ </div>
+ </div>
+ <Toggle
+ v-model="discoveryConfig.autoconnect_announces_to_internal"
+ class="shrink-0 sm:my-auto"
+ />
+ </div>
+ </div>
<div class="sm:col-span-2">
<div
class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between"
@@ -759,6 +839,10 @@ export default {
interface_discovery_blacklist: "",
required_discovery_value: null,
autoconnect_discovered_interfaces: null,
+ default_gravity: null,
+ autoconnect_interface_mode: "",
+ autoconnect_interface_gravity: null,
+ autoconnect_announces_to_internal: false,
default_bootstrap_only: false,
network_identity: "",
},
@@ -1324,6 +1408,22 @@ export default {
discovery.autoconnect_discovered_interfaces !== ""
? Number(discovery.autoconnect_discovered_interfaces)
: null;
+ this.discoveryConfig.default_gravity =
+ discovery.default_gravity !== undefined &&
+ discovery.default_gravity !== null &&
+ discovery.default_gravity !== ""
+ ? Number(discovery.default_gravity)
+ : null;
+ this.discoveryConfig.autoconnect_interface_mode = discovery.autoconnect_interface_mode ?? "";
+ this.discoveryConfig.autoconnect_interface_gravity =
+ discovery.autoconnect_interface_gravity !== undefined &&
+ discovery.autoconnect_interface_gravity !== null &&
+ discovery.autoconnect_interface_gravity !== ""
+ ? Number(discovery.autoconnect_interface_gravity)
+ : null;
+ this.discoveryConfig.autoconnect_announces_to_internal = this.parseBool(
+ discovery.autoconnect_announces_to_internal ?? false,
+ );
this.discoveryConfig.default_bootstrap_only = this.parseBool(discovery.default_bootstrap_only ?? false);
this.discoveryConfig.network_identity = discovery.network_identity ?? "";
} catch (e) {
@@ -1350,6 +1450,17 @@ export default {
this.discoveryConfig.autoconnect_discovered_interfaces === ""
? null
: Number(this.discoveryConfig.autoconnect_discovered_interfaces),
+ default_gravity:
+ this.discoveryConfig.default_gravity === null || this.discoveryConfig.default_gravity === ""
+ ? null
+ : Number(this.discoveryConfig.default_gravity),
+ autoconnect_interface_mode: this.discoveryConfig.autoconnect_interface_mode || null,
+ autoconnect_interface_gravity:
+ this.discoveryConfig.autoconnect_interface_gravity === null ||
+ this.discoveryConfig.autoconnect_interface_gravity === ""
+ ? null
+ : Number(this.discoveryConfig.autoconnect_interface_gravity),
+ autoconnect_announces_to_internal: this.discoveryConfig.autoconnect_announces_to_internal === true,
default_bootstrap_only: this.discoveryConfig.default_bootstrap_only,
network_identity: this.discoveryConfig.network_identity || null,
};

diff --git a/meshchatx/src/frontend/locales/de.json b/meshchatx/src/frontend/locales/de.json
index d75dc241..a70a4ac7 100644
--- a/meshchatx/src/frontend/locales/de.json
+++ b/meshchatx/src/frontend/locales/de.json
@@ -1417,6 +1417,19 @@
"recursive_prs_hint": "Erlaubt rekursive Pfadfindung auf dieser Schnittstelle unabhängig vom Modus (RNS recursive_prs).",
"announces_from_internal_label": "Announces von internen Schnittstellen weiterleiten",
"announces_from_internal_hint": "Wenn aus, werden Announces von Internal-Mode-Schnittstellen hier nicht weitergesendet (RNS announces_from_internal).",
+ "announces_to_internal_label": "Announces an interne Schnittstellen weiterleiten",
+ "announces_to_internal_hint": "Erlaubt dieser Schnittstelle, Announces an Internal-Mode-Schnittstellen weiterzuleiten (RNS announces_to_internal).",
+ "gravity_label": "Pfad-Gravitation",
+ "gravity_hint": "Pfad-Affinitat dieser Schnittstelle. Hoeher bevorzugt diesen Pfad. Leer lassen fuer default_gravity (RNS gravity).",
+ "gravity_placeholder": "0",
+ "default_gravity_label": "Standard-Gravitation",
+ "default_gravity_hint": "Standard-Pfadgravitation fuer Schnittstellen ohne eigene gravity (RNS default_gravity).",
+ "autoconnect_interface_mode_label": "Auto-Connect Schnittstellenmodus",
+ "autoconnect_interface_mode_hint": "Modus fuer automatisch verbundene entdeckte Schnittstellen (RNS autoconnect_interface_mode).",
+ "autoconnect_interface_gravity_label": "Auto-Connect Gravitation",
+ "autoconnect_interface_gravity_hint": "Gravitation fuer automatisch verbundene entdeckte Schnittstellen (RNS autoconnect_interface_gravity).",
+ "autoconnect_announces_to_internal_label": "Auto-Connect Announces an intern",
+ "autoconnect_announces_to_internal_hint": "Laesst auto-verbundene Schnittstellen Announces an Internal-Mode-Schnittstellen weiterleiten.",
"location_cmd_label": "Standortbefehl (optional)",
"location_cmd_placeholder": "/path/to/gps-script",
"location_cmd_hint": "Absoluter Pfad zu einem Programm, das latitude,longitude,height ausgibt. Reticulum startet es für Discovery-Announces (RNS location_cmd).",
@@ -1638,6 +1651,7 @@
"spreading_factor": "SF",
"coding_rate": "Kodierungsrate",
"bitrate": "Bitrate",
+ "gravity": "Gravitation",
"tx": "TX",
"rx": "RX",
"noise": "Rauschen",

diff --git a/meshchatx/src/frontend/locales/en.json b/meshchatx/src/frontend/locales/en.json
index 3c71a827..67caea91 100644
--- a/meshchatx/src/frontend/locales/en.json
+++ b/meshchatx/src/frontend/locales/en.json
@@ -1292,6 +1292,19 @@
"recursive_prs_hint": "Allow recursive path discovery on this interface regardless of mode (RNS recursive_prs).",
"announces_from_internal_label": "Propagate announces from internal interfaces",
"announces_from_internal_hint": "When off, announces received on internal-mode interfaces are not rebroadcast on this interface (RNS announces_from_internal).",
+ "announces_to_internal_label": "Propagate announces to internal interfaces",
+ "announces_to_internal_hint": "Allow this interface to forward announces toward internal-mode interfaces (RNS announces_to_internal).",
+ "gravity_label": "Path gravity",
+ "gravity_hint": "Pathing affinity for this interface. Higher prefers this path. Leave blank for default_gravity (RNS gravity).",
+ "gravity_placeholder": "0",
+ "default_gravity_label": "Default gravity",
+ "default_gravity_hint": "Default path gravity for interfaces without an explicit gravity (RNS default_gravity).",
+ "autoconnect_interface_mode_label": "Auto-connect interface mode",
+ "autoconnect_interface_mode_hint": "Mode applied to auto-connected discovered interfaces (RNS autoconnect_interface_mode).",
+ "autoconnect_interface_gravity_label": "Auto-connect gravity",
+ "autoconnect_interface_gravity_hint": "Gravity for auto-connected discovered interfaces (RNS autoconnect_interface_gravity).",
+ "autoconnect_announces_to_internal_label": "Auto-connect announces to internal",
+ "autoconnect_announces_to_internal_hint": "Let auto-connected interfaces propagate announces to internal-mode interfaces.",
"location_cmd_label": "Location command (optional)",
"location_cmd_placeholder": "/path/to/gps-script",
"location_cmd_hint": "Absolute path to an executable that prints latitude,longitude,height. Reticulum runs it for discovery announces (RNS location_cmd).",
@@ -1586,6 +1599,7 @@
"spreading_factor": "SF",
"coding_rate": "Coding Rate",
"bitrate": "Bitrate",
+ "gravity": "Gravity",
"tx": "TX",
"rx": "RX",
"noise": "Noise",

diff --git a/meshchatx/src/frontend/locales/es.json b/meshchatx/src/frontend/locales/es.json
index 0953ac53..7038b8b3 100644
--- a/meshchatx/src/frontend/locales/es.json
+++ b/meshchatx/src/frontend/locales/es.json
@@ -1365,6 +1365,19 @@
"recursive_prs_hint": "Permite descubrimiento recursivo de rutas en esta interfaz sin importar el modo (RNS recursive_prs).",
"announces_from_internal_label": "Propagar announces desde interfaces internas",
"announces_from_internal_hint": "Si está desactivado, los announces recibidos en interfaces en modo internal no se reemiten aquí (RNS announces_from_internal).",
+ "announces_to_internal_label": "Propagar announces hacia interfaces internas",
+ "announces_to_internal_hint": "Permite que esta interfaz reenvie announces hacia interfaces en modo internal (RNS announces_to_internal).",
+ "gravity_label": "Gravedad de ruta",
+ "gravity_hint": "Afinidad de ruta de esta interfaz. Valores altos preferencian esta ruta. Vacio usa default_gravity (RNS gravity).",
+ "gravity_placeholder": "0",
+ "default_gravity_label": "Gravedad por defecto",
+ "default_gravity_hint": "Gravedad de ruta por defecto para interfaces sin gravity explicita (RNS default_gravity).",
+ "autoconnect_interface_mode_label": "Modo de interfaz auto-conectada",
+ "autoconnect_interface_mode_hint": "Modo aplicado a interfaces descubiertas auto-conectadas (RNS autoconnect_interface_mode).",
+ "autoconnect_interface_gravity_label": "Gravedad de auto-conexion",
+ "autoconnect_interface_gravity_hint": "Gravedad para interfaces descubiertas auto-conectadas (RNS autoconnect_interface_gravity).",
+ "autoconnect_announces_to_internal_label": "Auto-conexion announces a internas",
+ "autoconnect_announces_to_internal_hint": "Permite que las interfaces auto-conectadas propaguen announces hacia interfaces en modo internal.",
"location_cmd_label": "Comando de ubicación (opcional)",
"location_cmd_placeholder": "/path/to/gps-script",
"location_cmd_hint": "Ruta absoluta a un ejecutable que imprime latitude,longitude,height. Reticulum lo ejecuta para announces de discovery (RNS location_cmd).",
@@ -1586,6 +1599,7 @@
"spreading_factor": "SF",
"coding_rate": "Tasa de codificación",
"bitrate": "Tasa de bits",
+ "gravity": "Gravedad",
"tx": "TX",
"rx": "RX",
"noise": "Ruido",

diff --git a/meshchatx/src/frontend/locales/fi.json b/meshchatx/src/frontend/locales/fi.json
index c45543a6..8f6c06d8 100644
--- a/meshchatx/src/frontend/locales/fi.json
+++ b/meshchatx/src/frontend/locales/fi.json
@@ -1365,6 +1365,19 @@
"recursive_prs_hint": "Sallii rekursiivisen polunlöydön tällä liittymällä tilasta riippumatta (RNS recursive_prs).",
"announces_from_internal_label": "Välitä announceja sisäisiltä liittymiltä",
"announces_from_internal_hint": "Kun pois päältä, internal-tilan liittymiltä saatuja announceja ei lähetetä uudelleen tällä liittymällä (RNS announces_from_internal).",
+ "announces_to_internal_label": "Valita announceja sisaisille liittymille",
+ "announces_to_internal_hint": "Sallii taman liittyman valittaa announceja internal-tilan liittymille (RNS announces_to_internal).",
+ "gravity_label": "Polun gravitaatio",
+ "gravity_hint": "Liittyman polun affiniteetti. Suurempi suosii tata polkua. Tyhja kayttaa default_gravity (RNS gravity).",
+ "gravity_placeholder": "0",
+ "default_gravity_label": "Oletusgravitaatio",
+ "default_gravity_hint": "Oletus polun gravitaatio liittymille ilman omaa gravity-arvoa (RNS default_gravity).",
+ "autoconnect_interface_mode_label": "Automaattiyhteyden liittymatila",
+ "autoconnect_interface_mode_hint": "Tila automaattisesti yhdistetyille loydetyille liittymille (RNS autoconnect_interface_mode).",
+ "autoconnect_interface_gravity_label": "Automaattiyhteyden gravitaatio",
+ "autoconnect_interface_gravity_hint": "Gravitaatio automaattisesti yhdistetyille loydetyille liittymille (RNS autoconnect_interface_gravity).",
+ "autoconnect_announces_to_internal_label": "Automaattiyhteys announces sisaisille",
+ "autoconnect_announces_to_internal_hint": "Sallii automaattisesti yhdistettyjen liittymien valittaa announceja internal-tilan liittymille.",
"location_cmd_label": "Sijaintikomento (valinnainen)",
"location_cmd_placeholder": "/path/to/gps-script",
"location_cmd_hint": "Absoluuttinen polku ohjelmaan, joka tulostaa latitude,longitude,height. Reticulum ajaa sen discovery-announceja varten (RNS location_cmd).",
@@ -1586,6 +1599,7 @@
"spreading_factor": "SF",
"coding_rate": "Koodaustaajuus",
"bitrate": "Bittinopeus",
+ "gravity": "Gravitaatio",
"tx": "TX",
"rx": "RX",
"noise": "Kohina",

diff --git a/meshchatx/src/frontend/locales/fr.json b/meshchatx/src/frontend/locales/fr.json
index 8c6f75ad..ffd85d33 100644
--- a/meshchatx/src/frontend/locales/fr.json
+++ b/meshchatx/src/frontend/locales/fr.json
@@ -1365,6 +1365,19 @@
"recursive_prs_hint": "Autorise la discovery récursive de chemins sur cette interface quel que soit le mode (RNS recursive_prs).",
"announces_from_internal_label": "Propager les announces depuis les interfaces internes",
"announces_from_internal_hint": "Si désactivé, les announces reçues sur des interfaces en mode internal ne sont pas réémises ici (RNS announces_from_internal).",
+ "announces_to_internal_label": "Propager les announces vers les interfaces internes",
+ "announces_to_internal_hint": "Autorise cette interface a propager des announces vers les interfaces en mode internal (RNS announces_to_internal).",
+ "gravity_label": "Gravite de chemin",
+ "gravity_hint": "Affinite de chemin de cette interface. Plus eleve prefere ce chemin. Vide utilise default_gravity (RNS gravity).",
+ "gravity_placeholder": "0",
+ "default_gravity_label": "Gravite par defaut",
+ "default_gravity_hint": "Gravite de chemin par defaut pour les interfaces sans gravity explicite (RNS default_gravity).",
+ "autoconnect_interface_mode_label": "Mode d interface auto-connectee",
+ "autoconnect_interface_mode_hint": "Mode applique aux interfaces decouvertes auto-connectees (RNS autoconnect_interface_mode).",
+ "autoconnect_interface_gravity_label": "Gravite d auto-connexion",
+ "autoconnect_interface_gravity_hint": "Gravite pour les interfaces decouvertes auto-connectees (RNS autoconnect_interface_gravity).",
+ "autoconnect_announces_to_internal_label": "Auto-connexion announces vers internal",
+ "autoconnect_announces_to_internal_hint": "Laisse les interfaces auto-connectees propager des announces vers les interfaces en mode internal.",
"location_cmd_label": "Commande de localisation (optionnel)",
"location_cmd_placeholder": "/path/to/gps-script",
"location_cmd_hint": "Chemin absolu vers un exécutable qui affiche latitude,longitude,height. Reticulum l'exécute pour les announces de discovery (RNS location_cmd).",
@@ -1586,6 +1599,7 @@
"spreading_factor": "SF",
"coding_rate": "Taux de codification",
"bitrate": "Débit",
+ "gravity": "Gravite",
"tx": "TX",
"rx": "RX",
"noise": "Bruit",

diff --git a/meshchatx/src/frontend/locales/it.json b/meshchatx/src/frontend/locales/it.json
index ec9136ef..c26e73c1 100644
--- a/meshchatx/src/frontend/locales/it.json
+++ b/meshchatx/src/frontend/locales/it.json
@@ -1417,6 +1417,19 @@
"recursive_prs_hint": "Consente la discovery ricorsiva dei percorsi su questa interfaccia indipendentemente dalla modalita (RNS recursive_prs).",
"announces_from_internal_label": "Propagare gli announce dalle interfacce interne",
"announces_from_internal_hint": "Se disattivato, gli announce ricevuti su interfacce in modalita internal non vengono ritrasmessi qui (RNS announces_from_internal).",
+ "announces_to_internal_label": "Propagare gli announce verso interfacce interne",
+ "announces_to_internal_hint": "Consente a questa interfaccia di inoltrare gli announce verso interfacce in modalita internal (RNS announces_to_internal).",
+ "gravity_label": "Gravita del percorso",
+ "gravity_hint": "Affinita di percorso di questa interfaccia. Valori alti preferiscono questo percorso. Vuoto usa default_gravity (RNS gravity).",
+ "gravity_placeholder": "0",
+ "default_gravity_label": "Gravita predefinita",
+ "default_gravity_hint": "Gravita di percorso predefinita per interfacce senza gravity esplicita (RNS default_gravity).",
+ "autoconnect_interface_mode_label": "Modalita interfaccia auto-connessa",
+ "autoconnect_interface_mode_hint": "Modalita applicata alle interfacce scoperte auto-connesse (RNS autoconnect_interface_mode).",
+ "autoconnect_interface_gravity_label": "Gravita auto-connessione",
+ "autoconnect_interface_gravity_hint": "Gravita per interfacce scoperte auto-connesse (RNS autoconnect_interface_gravity).",
+ "autoconnect_announces_to_internal_label": "Auto-connessione announce verso internal",
+ "autoconnect_announces_to_internal_hint": "Consente alle interfacce auto-connesse di propagare gli announce verso interfacce in modalita internal.",
"location_cmd_label": "Comando posizione (opzionale)",
"location_cmd_placeholder": "/path/to/gps-script",
"location_cmd_hint": "Percorso assoluto a un eseguibile che stampa latitude,longitude,height. Reticulum lo esegue per gli announce di discovery (RNS location_cmd).",
@@ -1638,6 +1651,7 @@
"spreading_factor": "SF",
"coding_rate": "Codifica",
"bitrate": "Bitrate",
+ "gravity": "Gravita",
"tx": "TX",
"rx": "RX",
"noise": "Rumore",

diff --git a/meshchatx/src/frontend/locales/nl.json b/meshchatx/src/frontend/locales/nl.json
index 6f08644a..989c5be7 100644
--- a/meshchatx/src/frontend/locales/nl.json
+++ b/meshchatx/src/frontend/locales/nl.json
@@ -1365,6 +1365,19 @@
"recursive_prs_hint": "Staat recursieve padontdekking toe op deze interface ongeacht de modus (RNS recursive_prs).",
"announces_from_internal_label": "Announces van interne interfaces doorgeven",
"announces_from_internal_hint": "Indien uit, worden announces ontvangen op internal-mode interfaces hier niet opnieuw uitgezonden (RNS announces_from_internal).",
+ "announces_to_internal_label": "Announces naar interne interfaces doorgeven",
+ "announces_to_internal_hint": "Laat deze interface announces doorsturen naar internal-mode interfaces (RNS announces_to_internal).",
+ "gravity_label": "Padzwaartekracht",
+ "gravity_hint": "Padaffiniteit van deze interface. Hoger geeft voorkeur aan dit pad. Leeg gebruikt default_gravity (RNS gravity).",
+ "gravity_placeholder": "0",
+ "default_gravity_label": "Standaard zwaartekracht",
+ "default_gravity_hint": "Standaard padzwaartekracht voor interfaces zonder eigen gravity (RNS default_gravity).",
+ "autoconnect_interface_mode_label": "Auto-connect interfacemodus",
+ "autoconnect_interface_mode_hint": "Modus voor auto-connected ontdekte interfaces (RNS autoconnect_interface_mode).",
+ "autoconnect_interface_gravity_label": "Auto-connect zwaartekracht",
+ "autoconnect_interface_gravity_hint": "Zwaartekracht voor auto-connected ontdekte interfaces (RNS autoconnect_interface_gravity).",
+ "autoconnect_announces_to_internal_label": "Auto-connect announces naar intern",
+ "autoconnect_announces_to_internal_hint": "Laat auto-connected interfaces announces doorsturen naar internal-mode interfaces.",
"location_cmd_label": "Locatiecommando (optioneel)",
"location_cmd_placeholder": "/path/to/gps-script",
"location_cmd_hint": "Absoluut pad naar een uitvoerbaar bestand dat latitude,longitude,height print. Reticulum voert dit uit voor discovery-announces (RNS location_cmd).",
@@ -1586,6 +1599,7 @@
"spreading_factor": "SF",
"coding_rate": "Coderingspercentage",
"bitrate": "Bitsnelheid",
+ "gravity": "Zwaartekracht",
"tx": "TX",
"rx": "RX",
"noise": "Geluid",

diff --git a/meshchatx/src/frontend/locales/ru.json b/meshchatx/src/frontend/locales/ru.json
index ce31a691..d97b8691 100644
--- a/meshchatx/src/frontend/locales/ru.json
+++ b/meshchatx/src/frontend/locales/ru.json
@@ -1417,6 +1417,19 @@
"recursive_prs_hint": "Разрешает рекурсивный поиск путей на этом интерфейсе вне зависимости от режима (RNS recursive_prs).",
"announces_from_internal_label": "Распространять announces с внутренних интерфейсов",
"announces_from_internal_hint": "Если выключено, announces с интерфейсов в режиме internal не ретранслируются здесь (RNS announces_from_internal).",
+ "announces_to_internal_label": "Распространять announces на внутренние интерфейсы",
+ "announces_to_internal_hint": "Разрешает этому интерфейсу пересылать announces на интерфейсы в режиме internal (RNS announces_to_internal).",
+ "gravity_label": "Гравитация пути",
+ "gravity_hint": "Сродство пути для этого интерфейса. Больше значение предпочитает этот путь. Пусто = default_gravity (RNS gravity).",
+ "gravity_placeholder": "0",
+ "default_gravity_label": "Гравитация по умолчанию",
+ "default_gravity_hint": "Гравитация пути по умолчанию для интерфейсов без явного gravity (RNS default_gravity).",
+ "autoconnect_interface_mode_label": "Режим автоподключаемого интерфейса",
+ "autoconnect_interface_mode_hint": "Режим для автоподключённых обнаруженных интерфейсов (RNS autoconnect_interface_mode).",
+ "autoconnect_interface_gravity_label": "Гравитация автоподключения",
+ "autoconnect_interface_gravity_hint": "Гравитация для автоподключённых обнаруженных интерфейсов (RNS autoconnect_interface_gravity).",
+ "autoconnect_announces_to_internal_label": "Автоподключение announces к internal",
+ "autoconnect_announces_to_internal_hint": "Позволяет автоподключённым интерфейсам пересылать announces на интерфейсы в режиме internal.",
"location_cmd_label": "Команда местоположения (необязательно)",
"location_cmd_placeholder": "/path/to/gps-script",
"location_cmd_hint": "Абсолютный путь к исполняемому файлу, печатающему latitude,longitude,height. Reticulum запускает его для discovery-announces (RNS location_cmd).",
@@ -1638,6 +1651,7 @@
"spreading_factor": "SF",
"coding_rate": "Скорость кодирования",
"bitrate": "Битрейт",
+ "gravity": "Гравитация",
"tx": "TX",
"rx": "RX",
"noise": "Шум",

diff --git a/meshchatx/src/frontend/locales/zh.json b/meshchatx/src/frontend/locales/zh.json
index 3f2c4006..56face58 100644
--- a/meshchatx/src/frontend/locales/zh.json
+++ b/meshchatx/src/frontend/locales/zh.json
@@ -1365,6 +1365,19 @@
"recursive_prs_hint": "无论接口模式如何,都允许在此接口上进行递归路径发现(RNS recursive_prs)。",
"announces_from_internal_label": "传播来自内部接口的通告",
"announces_from_internal_hint": "关闭后,在 internal 模式接口上收到的通告不会在此接口上重播(RNS announces_from_internal)。",
+ "announces_to_internal_label": "向内部接口传播通告",
+ "announces_to_internal_hint": "允许此接口将通告转发到 internal 模式接口(RNS announces_to_internal)。",
+ "gravity_label": "路径重力",
+ "gravity_hint": "此接口的路径亲和性。更高更优先此路径。留空使用 default_gravity(RNS gravity)。",
+ "gravity_placeholder": "0",
+ "default_gravity_label": "默认重力",
+ "default_gravity_hint": "未显式设置 gravity 的接口的默认路径重力(RNS default_gravity)。",
+ "autoconnect_interface_mode_label": "自动连接接口模式",
+ "autoconnect_interface_mode_hint": "应用于自动连接的已发现接口的模式(RNS autoconnect_interface_mode)。",
+ "autoconnect_interface_gravity_label": "自动连接重力",
+ "autoconnect_interface_gravity_hint": "自动连接的已发现接口的重力(RNS autoconnect_interface_gravity)。",
+ "autoconnect_announces_to_internal_label": "自动连接通告到内部",
+ "autoconnect_announces_to_internal_hint": "允许自动连接的接口将通告传播到 internal 模式接口。",
"location_cmd_label": "位置命令(可选)",
"location_cmd_placeholder": "/path/to/gps-script",
"location_cmd_hint": "可执行文件的绝对路径,输出 latitude,longitude,height。Reticulum 会在发现通告时运行它(RNS location_cmd)。",
@@ -1586,6 +1599,7 @@
"spreading_factor": "系统框架",
"coding_rate": "编码率",
"bitrate": "比特率",
+ "gravity": "重力",
"tx": "TX 主题",
"rx": "RX 自动调用",
"noise": "噪声",

diff --git a/pyproject.toml b/pyproject.toml
index d9ba3a42..7e001e79 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -24,7 +24,7 @@ dependencies = [
"lxmf>=1.1.0",
"psutil>=7.2.2",
"pyserial>=3.5",
- "rns>=1.4.0",
+ "rns>=1.4.1",
"websockets>=16.0",
"bcrypt>=5.0.0,<6.0.0",
"aiohttp-session>=2.12.1,<3.0.0",

diff --git a/requirements.txt b/requirements.txt
index 15502c6a..c46e73c6 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -118,6 +118,10 @@ aiosignal==1.4.0 \
--hash=sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e \
--hash=sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7
# via aiohttp
+anyio==4.14.2 \
+ --hash=sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494 \
+ --hash=sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f
+ # via httpx
attrs==26.1.0 \
--hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \
--hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32
@@ -287,6 +291,12 @@ cbor2==6.1.1 \
--hash=sha256:fc389db70aa0f4b639bf4605b6d7f8ffcce5a613fbfece84a6312e89e6bb417b \
--hash=sha256:fd9d300ad983b860fbfb0ab148ddd3a379be25430bb141ad41344adc1c0792c1
# via reticulum-meshchatx
+certifi==2026.7.22 \
+ --hash=sha256:62f22742b58a1a33014a2b6b706588a8d7e2a88ae7bd1a6ebe8c992928483775 \
+ --hash=sha256:741e2c3b351ddf169a738da9f2c048608ff7f2c5cc02f1ebc6b118bb090d5d55
+ # via
+ # httpcore
+ # httpx
cffi==2.0.0 \
--hash=sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb \
--hash=sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b \
@@ -538,10 +548,37 @@ frozenlist==1.8.0 \
# via
# aiohttp
# aiosignal
+h11==0.16.0 \
+ --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \
+ --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86
+ # via httpcore
+h2==4.4.0 \
+ --hash=sha256:46b551bdcdc7e83cf5c04d0bf93badb8a939bd2287d9fee1abb23a445b9e0580 \
+ --hash=sha256:6acffe1aeab79098d7eb0f8385c1add11f2c7a94815f6fa2b7060eeddee3d87c
+ # via httpx
+hpack==4.2.0 \
+ --hash=sha256:0895cfa3b5531fc65fe439c05eb65144f123bf7a394fcaa56aa423548d8e45c0 \
+ --hash=sha256:858ac0b02280fa582b5080d68db0899c62a80375e0e5413a74970c5e518b6986
+ # via h2
+httpcore==1.0.9 \
+ --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \
+ --hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8
+ # via httpx
+httpx==0.28.1 \
+ --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \
+ --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad
+ # via reticulum-meshchatx
+hyperframe==6.1.0 \
+ --hash=sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5 \
+ --hash=sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08
+ # via h2
idna==3.18 \
--hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \
--hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848
- # via yarl
+ # via
+ # anyio
+ # httpx
+ # yarl
lxmf==1.1.0 \
--hash=sha256:187e5cba257163ba1cffa1b49a33dc8893cf3b982397ccd77461ceaa5cd54af1 \
--hash=sha256:3836550364e059aa94eb955f9a862d20287643518d7a0b2a167b3b58c1d07cbd
@@ -985,9 +1022,9 @@ pyserial==3.5 \
# via
# reticulum-meshchatx
# rns
-rns==1.4.0 \
- --hash=sha256:a88c6ae15c289867b2d8ee8f6ba4363f4dc728283f892b0790dba226f7dddcae \
- --hash=sha256:fa9e76d0a78bf253eae66137e6bbdc65f470db3950a95b034ce32ca845ff0e44
+rns==1.4.1 \
+ --hash=sha256:7093abd4125f574796c797f4f89f22093d2cbc43281f941939362b2dd079351e \
+ --hash=sha256:d92f827cb77df8eda2824153a4d1288e8e2ea588382166f4ca45427288d0ee6b
# via
# lxmf
# lxst
@@ -998,6 +1035,7 @@ typing-extensions==4.15.0 ; python_full_version < '3.13' or sys_platform == 'win
# via
# aiohttp
# aiosignal
+ # anyio
# bleak
# winrt-runtime
wasmtime==46.0.1 \

diff --git a/scripts/build-android-wheels-local.sh b/scripts/build-android-wheels-local.sh
index 184d186c..d80131a4 100755
--- a/scripts/build-android-wheels-local.sh
+++ b/scripts/build-android-wheels-local.sh
@@ -33,7 +33,7 @@ Options:
--lxst-version V LXST wheel version for metadata patch (default: 0.5.0)
--bleak-version V bleak pure-python wheel version to vendor (default: 3.0.2)
--httpx-version V httpx pure-python wheel version to vendor (default: 0.28.1)
- --rns-version V rns wheel version to patch (default: 1.4.0)
+ --rns-version V rns wheel version to patch (default: 1.4.1)
--no-lxst-patch Skip LXST metadata patch
--no-rns-patch Skip RNS Android RNodeInterface patch
--only-recipes LIST Comma-separated recipe directory names under
@@ -64,7 +64,7 @@ NUMPY_VERSION="1.26.2"
LXST_VERSION="0.5.0"
BLEAK_VERSION="3.0.2"
HTTPX_VERSION="0.28.1"
-RNS_VERSION="1.4.0"
+RNS_VERSION="1.4.1"
PATCH_LXST="1"
PATCH_RNS="1"
ONLY_RECIPES=""

diff --git a/tests/backend/test_interface_discovery.py b/tests/backend/test_interface_discovery.py
index 94fddc92..7b3832bc 100644
--- a/tests/backend/test_interface_discovery.py
+++ b/tests/backend/test_interface_discovery.py
@@ -58,6 +58,10 @@ async def test_reticulum_discovery_get_and_patch(temp_dir):
"interface_discovery_blacklist": "tcp-bad,*:9999",
"required_discovery_value": "16",
"autoconnect_discovered_interfaces": "2",
+ "default_gravity": "1",
+ "autoconnect_interface_mode": "gateway",
+ "autoconnect_interface_gravity": "2",
+ "autoconnect_announces_to_internal": "yes",
"default_bootstrap_only": "yes",
"network_identity": "/tmp/net_id",
},
@@ -107,6 +111,10 @@ async def test_reticulum_discovery_get_and_patch(temp_dir):
)
assert get_data["discovery"]["required_discovery_value"] == "16"
assert get_data["discovery"]["autoconnect_discovered_interfaces"] == "2"
+ assert get_data["discovery"]["default_gravity"] == "1"
+ assert get_data["discovery"]["autoconnect_interface_mode"] == "gateway"
+ assert get_data["discovery"]["autoconnect_interface_gravity"] == "2"
+ assert get_data["discovery"]["autoconnect_announces_to_internal"] == "yes"
assert get_data["discovery"]["default_bootstrap_only"] is True
assert get_data["discovery"]["network_identity"] == "/tmp/net_id"
@@ -118,6 +126,10 @@ async def test_reticulum_discovery_get_and_patch(temp_dir):
"interface_discovery_blacklist": "",
"required_discovery_value": 18,
"autoconnect_discovered_interfaces": 5,
+ "default_gravity": 0,
+ "autoconnect_interface_mode": "boundary",
+ "autoconnect_interface_gravity": -1,
+ "autoconnect_announces_to_internal": False,
"default_bootstrap_only": False,
"network_identity": "/tmp/other_id",
}
@@ -140,6 +152,10 @@ async def test_reticulum_discovery_get_and_patch(temp_dir):
assert patch_data["discovery"]["autoconnect_discovered_interfaces"] == (
ReticulumMeshChat.DEFAULT_AUTOCONNECT_DISCOVERED_INTERFACES
)
+ assert patch_data["discovery"]["default_gravity"] == 0
+ assert patch_data["discovery"]["autoconnect_interface_mode"] == "boundary"
+ assert patch_data["discovery"]["autoconnect_interface_gravity"] == -1
+ assert patch_data["discovery"]["autoconnect_announces_to_internal"] == "no"
assert patch_data["discovery"]["default_bootstrap_only"] is False
assert patch_data["discovery"]["network_identity"] == "/tmp/other_id"
assert config["reticulum"]["discover_interfaces"] is False
@@ -148,6 +164,10 @@ async def test_reticulum_discovery_get_and_patch(temp_dir):
assert "interface_discovery_blacklist" not in config["reticulum"]
assert config["reticulum"]["required_discovery_value"] == 18
assert "autoconnect_discovered_interfaces" not in config["reticulum"]
+ assert config["reticulum"]["default_gravity"] == 0
+ assert config["reticulum"]["autoconnect_interface_mode"] == "boundary"
+ assert config["reticulum"]["autoconnect_interface_gravity"] == -1
+ assert config["reticulum"]["autoconnect_announces_to_internal"] == "no"
assert "default_bootstrap_only" not in config["reticulum"]
assert app_instance.current_context.config.default_bootstrap_only.get() is False
assert config["reticulum"]["network_identity"] == "/tmp/other_id"

diff --git a/tests/backend/test_interface_editor.py b/tests/backend/test_interface_editor.py
index 82653c9b..16b5df55 100644
--- a/tests/backend/test_interface_editor.py
+++ b/tests/backend/test_interface_editor.py
@@ -138,6 +138,16 @@ def test_apply_yes_no_option_recursive_prs():
assert details["announces_from_internal"] == "no"
+def test_sanitize_imported_gravity_and_announces_to_internal():
+ body = {
+ "gravity": "3",
+ "announces_to_internal": True,
+ }
+ assert InterfaceEditor.sanitize_imported_rns_options(body) is None
+ assert body["gravity"] == 3
+ assert body["announces_to_internal"] == "yes"
+
+
def test_validate_location_cmd_rejects_shell_metacharacters():
assert InterfaceEditor.validate_location_cmd("/usr/bin/true") is None
assert InterfaceEditor.validate_location_cmd("~/bin/gps.sh") is None

diff --git a/tests/backend/test_interface_options.py b/tests/backend/test_interface_options.py
index e729312a..86604059 100644
--- a/tests/backend/test_interface_options.py
+++ b/tests/backend/test_interface_options.py
@@ -763,6 +763,28 @@ async def test_internal_mode_and_rns_bool_options_persist(temp_dir):
assert saved["announces_from_internal"] == "no"
+@pytest.mark.asyncio
+async def test_gravity_and_announces_to_internal_persist(temp_dir):
+ config = ConfigDict({"reticulum": {}, "interfaces": {}})
+
+ free_port = _free_port("tcp")
+ async with make_app(temp_dir, config) as handler:
+ payload = {
+ "name": "GravityTCP",
+ "type": "TCPServerInterface",
+ "listen_ip": "127.0.0.1",
+ "listen_port": free_port,
+ "gravity": 2,
+ "announces_to_internal": True,
+ }
+ response = await handler(make_request(payload))
+ body = json.loads(response.body)
+ assert response.status == 200, body
+ saved = config["interfaces"]["GravityTCP"]
+ assert saved["gravity"] == 2
+ assert saved["announces_to_internal"] == "yes"
+
+
@pytest.mark.asyncio
async def test_rejects_unknown_interface_mode(temp_dir):
config = ConfigDict({"reticulum": {}, "interfaces": {}})

diff --git a/tests/frontend/AddInterfaceOptions.test.js b/tests/frontend/AddInterfaceOptions.test.js
index 9cc77de0..2249da34 100644
--- a/tests/frontend/AddInterfaceOptions.test.js
+++ b/tests/frontend/AddInterfaceOptions.test.js
@@ -349,6 +349,27 @@ describe("AddInterfacePage.vue interface options", () => {
);
});
+ it("sends gravity and announces_to_internal", async () => {
+ const wrapper = mountPage();
+
+ wrapper.vm.newInterfaceName = "GravityLAN";
+ wrapper.vm.newInterfaceType = "TCPServerInterface";
+ wrapper.vm.newInterfaceListenIp = "127.0.0.1";
+ wrapper.vm.newInterfaceListenPort = 4242;
+ wrapper.vm.sharedInterfaceSettings.gravity = 2;
+ wrapper.vm.sharedInterfaceSettings.announces_to_internal = true;
+
+ await wrapper.vm.saveInterface();
+
+ expect(mockAxios.post).toHaveBeenCalledWith(
+ "/api/v1/reticulum/interfaces/add",
+ expect.objectContaining({
+ gravity: 2,
+ announces_to_internal: true,
+ })
+ );
+ });
+
it("sends BackboneInterface fast-flapping options in listener mode", async () => {
const wrapper = mountPage();

diff --git a/uv.lock b/uv.lock
index 6e6b2b60..4c4bc705 100644
--- a/uv.lock
+++ b/uv.lock
@@ -2055,7 +2055,7 @@ requires-dist = [
{ name = "psutil", specifier = ">=7.2.2" },
{ name = "pycparser", specifier = ">=3.0" },
{ name = "pyserial", specifier = ">=3.5" },
- { name = "rns", specifier = ">=1.4.0" },
+ { name = "rns", specifier = ">=1.4.1" },
{ name = "wasmtime", specifier = ">=28.0.0" },
{ name = "websockets", specifier = ">=16.0" },
]
@@ -2090,15 +2090,15 @@ wheels = [
[[package]]
name = "rns"
-version = "1.4.0"
+version = "1.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
{ name = "pyserial" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/3c/45/5aaed5107e0d3f434aada725224e1c54af96555959da312c01e12228c1de/rns-1.4.0.tar.gz", hash = "sha256:fa9e76d0a78bf253eae66137e6bbdc65f470db3950a95b034ce32ca845ff0e44", size = 519727, upload-time = "2026-07-20T17:13:03.624Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/be/c3/b7a9b48261f89453502ab88386acd60c1e542155bc7d8734031551417a3e/rns-1.4.1.tar.gz", hash = "sha256:7093abd4125f574796c797f4f89f22093d2cbc43281f941939362b2dd079351e", size = 522170, upload-time = "2026-07-24T23:11:00.374Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/56/4e/1a50351fba424b891a61e5f0fed9f3f631d7a034ae5edcc531d79562467c/rns-1.4.0-py3-none-any.whl", hash = "sha256:a88c6ae15c289867b2d8ee8f6ba4363f4dc728283f892b0790dba226f7dddcae", size = 606736, upload-time = "2026-07-20T17:12:58.403Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/a4/c187a6b9fc2305a9281573279f006bc78d87d1e7fe8315df3b1514c9e662/rns-1.4.1-py3-none-any.whl", hash = "sha256:d92f827cb77df8eda2824153a4d1288e8e2ea588382166f4ca45427288d0ee6b", size = 608512, upload-time = "2026-07-24T23:10:55.268Z" },
]
[[package]]

diff --git a/vendor/rns_filesync/README.md b/vendor/rns_filesync/README.md
index 892dd345..38d80a0c 100644
--- a/vendor/rns_filesync/README.md
+++ b/vendor/rns_filesync/README.md
@@ -51,7 +51,7 @@ pip install -e ".[dev]"
# or: make install-user
```
-Requires rns>=1.4.0. Current package version: **1.0.0**.
+Requires rns>=1.4.1. Current package version: **1.0.0**.
```bash
rns-filesync -v

diff --git a/vendor/rns_filesync/README_RU.md b/vendor/rns_filesync/README_RU.md
index 48d2f8cd..bc5040df 100644
--- a/vendor/rns_filesync/README_RU.md
+++ b/vendor/rns_filesync/README_RU.md
@@ -2,7 +2,7 @@
English README is the source of truth: [README.md](README.md).
-Кратко: библиотека и CLI для синхронизации каталогов через Reticulum (`rns>=1.4.0`).
+Кратко: библиотека и CLI для синхронизации каталогов через Reticulum (`rns>=1.4.1`).
Конфиг и ACL как у rngit: `~/.rns_filesync/config`, правила `r:hash` / `w:all`, файлы `.allowed`.
```bash

diff --git a/vendor/rns_filesync/pyproject.toml b/vendor/rns_filesync/pyproject.toml
index 9eaf375f..e52a499f 100644
--- a/vendor/rns_filesync/pyproject.toml
+++ b/vendor/rns_filesync/pyproject.toml
@@ -10,7 +10,7 @@ readme = "README.md"
license = { text = "BSD-2-Clause" }
requires-python = ">=3.10"
dependencies = [
- "rns>=1.4.0",
+ "rns>=1.4.1",
]
[project.optional-dependencies]

diff --git a/vendor/rns_filesync/requirements.txt b/vendor/rns_filesync/requirements.txt
index f50b3c94..8ccce534 100644
--- a/vendor/rns_filesync/requirements.txt
+++ b/vendor/rns_filesync/requirements.txt
@@ -1 +1 @@
-rns>=1.4.0
+rns>=1.4.1


──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────